
/*
 * Copyright (C) 2008-2009 Archie L. Cobbs. All rights reserved.
 *
 * $Id: BasicTest1.java 377 2010-03-11 20:50:18Z archie.cobbs $
 */

package org.dellroad.sidekar;

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;

import org.dellroad.sidekar.annotation.SidekarEntity;
import org.dellroad.sidekar.annotation.SidekarField;
import org.dellroad.sidekar.annotation.SidekarSet;
import org.dellroad.sidekar.util.VoidDatabaseAction;
import org.testng.annotations.Test;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;

public class BasicTest1 extends DatabaseTest {

    private static final Date SOME_DATE = new Date(1257478377000L);    // Thu Nov  6  3:32:57 UTC 2009

    private static final BigInteger SOME_BIGINT = new BigInteger("-1586064252425895610201908038346642667717271991890987206068066");

    private Top top;
    private Sub1 sub1a;
    private Sub1 sub1b;
    private Sub1 sub1c;
    private Sub2 sub2a;

    private Sub1 sub1d;
    private Sub2 sub2b;
    private Sub2 sub2c;

    private Sub1 sub1e;

    private AttachedField attachedField;

    private final String[] stringArray = new String[] { "foo", null, "", "\\&bar\"" + new String(new char[] { (char)0x0001 }) };
    private final boolean[] booleanArray = new boolean[] { true, false, true };
    private final byte[][] byteMultiArray = new byte[][] {
      null,
      new byte[] { (byte)0, (byte)1, (byte)-1, (byte)127, (byte)-128 }
    };
    private final int[][][] intMultiArray = new int[][][] {
      new int[][] {
        new int[] { 1, 2, 3 },
        null,
        new int[] { Integer.MIN_VALUE, 0, Integer.MAX_VALUE },
        new int[] { },
      },
      null,
      new int[][] { }
    };

    @Test
    public void testBasic1() {

        this.attachedField = this.db.getAttachedField();

        // Populate
        this.db.performTransaction(null, new VoidDatabaseAction() {
            public void executeVoid() {
                populateDB();
            }
        });

        // Verify
        this.db.performTransaction(null, new VoidDatabaseAction() {
            public void executeVoid() {
                verifyDB();
            }
        });

        // Export
        compareExport();

        // Check attach/detach operations
        this.db.performTransaction(null, new VoidDatabaseAction() {
            public void executeVoid() {
                checkAttachDetach();
            }
        });
    }

    protected void populateDB() {

        // Create a Top
        this.top = getTopEntity().getSingleton();
        this.top.setBoolean(true);
        this.top.setByte((byte)-37);
        this.top.setChar('!');
        this.top.setShort(Short.MAX_VALUE);
        this.top.setInt(12345678);
        this.top.setFloat(-123e8f);
        this.top.setLong(Long.MIN_VALUE);
        this.top.setDouble(-123e8);
        this.top.setString("foobar");
        assertNull(this.top.getDate());
        this.top.setDate(SOME_DATE);
        assertNull(this.top.getBigInt());
        this.top.setBigInt(SOME_BIGINT);
        this.top.setStringArray(this.stringArray);
        this.top.setBooleanArray(this.booleanArray);
        this.top.setByteMultiArray(this.byteMultiArray);
        this.top.setIntMultiArray(this.intMultiArray);

        // Create some Sub1s and Sub2s
        this.sub1a = getSub1Entity().create();
        this.sub1b = getSub1Entity().create();
        this.sub1c = getSub1Entity().create();
        this.sub2a = getSub2Entity().create();

        assert this.sub1a instanceof Top;
        assert this.sub2a instanceof Top;

        this.sub2a.getSub1Set().add(this.sub1a);
        this.sub2a.getSub1Set().add(null);
        this.sub2a.getSub1Set().add(this.sub1b);
        this.sub1a.setSub2(this.sub2a);
        this.sub1b.setSub2(this.sub2a);

        // Create an attached and a detached item that refers to itself
        this.sub1d = getSub1Entity().create();
        this.sub2b = getSub2Entity().createDetached();
        try {
            this.sub2b.getTopSet();
            assert false;
        } catch (DetachedItemException e) {
            // expected
        }

        // Attach detached item and set some fields
        this.sub2b.sidekarAttach();
        this.sub2b.setInt(123);
        this.sub2b.getTopSet().add(this.sub2b);
        long id = this.sub2b.getSidekarId();
        assert this.sub2b.getInt() == 123;
        assert this.sub2b.getTopSet().size() == 1;
        assert this.sub2b.getTopSet().iterator().next() == this.sub2b;

        // Verify we can't access a detached item through a reference
        this.sub2b.sidekarDetach();
        assertSame(this.sub2b.getSidekarEntity(), getSub2Entity());
        assertFalse(this.sub2b.isSidekarAttached());
        assertEquals(this.sub2b.getSidekarId(), id);
        try {
            this.sub2b.getInt();
            assert false;
        } catch (DetachedItemException e) {
            // expected
        }

        // Reattach item and fields should have reset
        this.sub2b.sidekarAttach();
        assertSame(this.sub2b.getSidekarEntity(), getSub2Entity());
        assertTrue(this.sub2b.isSidekarAttached());
        assertEquals(this.sub2b.getSidekarId(), id);
        assertEquals(this.sub2b.getInt(), 0);
        assertTrue(this.sub2b.getTopSet().isEmpty());
        this.sub2b.getTopSet().add(this.sub2b);

        // Create a detached item we never do anything with
        this.sub2c = getSub2Entity().createDetached();

        // Test array of entity type
        this.sub1e = getSub1Entity().create();
        this.top.setSub1Array(new Sub1[] { this.sub1e, null, this.sub1e });
    }

    protected void verifyDB() {

        // Check attachedness
        assert this.top.isSidekarAttached();
        assert this.sub1a.isSidekarAttached();
        assert this.sub1b.isSidekarAttached();
        assert this.sub1c.isSidekarAttached();
        assert this.sub2a.isSidekarAttached();
        assert this.attachedField.get(this.top);
        assert this.attachedField.get(this.sub1a);
        assert this.attachedField.get(this.sub1b);
        assert this.attachedField.get(this.sub1c);
        assert this.attachedField.get(this.sub2a);

        // Check instances
        Set<Top> allTops = getAll(Top.class);
        assertEquals((Object)allTops, mkset(this.top, this.sub1a,
          this.sub1b, this.sub1c, this.sub1d, this.sub1e, this.sub2a, this.sub2b));
        assertEquals((Object)getAll(Sub1.class), mkset(this.sub1a, this.sub1b, this.sub1c, this.sub1d, this.sub1e));
        assertEquals((Object)getAll(Sub2.class), mkset(this.sub2a, this.sub2b));
        assert getTopEntity().isInstance(getFirst(Top.class));
        assert getSub1Entity().isInstance(getFirst(Sub1.class));
        assert getSub2Entity().isInstance(getFirst(Sub2.class));

        // Check casting
        getTopEntity().cast(this.top);
        getTopEntity().cast(this.sub1a);
        getTopEntity().cast(this.sub2a);
        try {
            getSub1Entity().cast(this.top);
            assert false;
        } catch (ClassCastException e) {
            // expected
        }
        getSub1Entity().cast(this.sub1a);
        try {
            getSub1Entity().cast(this.sub2a);
            assert false;
        } catch (ClassCastException e) {
            // expected
        }
        try {
            getSub2Entity().cast(this.top);
            assert false;
        } catch (ClassCastException e) {
            // expected
        }
        try {
            getSub2Entity().cast(this.sub1a);
            assert false;
        } catch (ClassCastException e) {
            // expected
        }
        getSub2Entity().cast(this.sub2a);

        // Verify Top
        Top foundTop = null;
        for (Top someTop : allTops) {
            if (someTop.getSidekarEntity() == getTopEntity()) {
                foundTop = someTop;
                break;
            } else
                assert foundTop == null;
        }
        assertSame(foundTop, this.top);
        assertEquals(this.top.getBoolean(), true);
        assertEquals(this.top.getByte(), (byte)-37);
        assertEquals(this.top.getChar(), '!');
        assertEquals(this.top.getShort(), Short.MAX_VALUE);
        assertEquals(this.top.getInt(), 12345678);
        assertEquals(this.top.getFloat(), -123e8f);
        assertEquals(this.top.getLong(), Long.MIN_VALUE);
        assertEquals(this.top.getDouble(), -123e8);
        assertEquals(this.top.getString(), "foobar");
        assertEquals(this.top.getDate(), SOME_DATE);
        assertEquals(this.top.getBigInt(), SOME_BIGINT);
        assertEquals(this.top.getStringArray(), this.stringArray);
        assertTrue(Arrays.equals(this.top.getBooleanArray(), this.booleanArray));
        assertTrue(Arrays.deepEquals(this.top.getByteMultiArray(), this.byteMultiArray));
        assertTrue(Arrays.deepEquals(this.top.getIntMultiArray(), this.intMultiArray));
        assertEquals(this.top.getSub1Array().length, 3);
        assertSame(this.top.getSub1Array()[0], this.sub1e);
        assertNull(this.top.getSub1Array()[1]);
        assertSame(this.top.getSub1Array()[2], this.sub1e);

        // Verify references
        assertEquals((Object)this.sub2a.getSub1Set(), (Object)mkset(this.sub1a, this.sub1b, null));
        assertSame(this.sub1a.getSub2(), this.sub2a);
        assertSame(this.sub1b.getSub2(), this.sub2a);
        assertNull(this.sub1c.getSub2());

        // Check array stuff
        Sub1[] array = this.top.getSub1Array();
        array[0] = null;        // this should not affect array property
        assertNotNull(this.top.getSub1Array()[0]);
        this.top.setSub1Array(array);
        this.top.setSub1Array(null);

        // Check counts
        assertEquals(this.db.getEntity(Top.class).count(), 8);
        assertEquals(this.db.getEntity(Sub1.class).count(), 5);
        assertEquals(this.db.getEntity(Sub2.class).count(), 2);
    }

    protected void checkAttachDetach() {

        // Check detaching
        assert this.top.isSidekarAttached();
        assert this.attachedField.get(this.top);
        this.top.sidekarDetach();
        assert !this.top.isSidekarAttached();
        assert !this.attachedField.get(this.top);
        try {
            this.top.getInt();
            assert false;
        } catch (DetachedItemException e) {
            // expected
        }
        assert this.sub1a.isSidekarAttached();
        assert this.attachedField.get(this.sub1a);

        // Check attach/detach stuff
        assert this.sub1d.isSidekarAttached();
        assert this.attachedField.get(this.sub1d);
        assert this.sub2b.isSidekarAttached();
        assert this.attachedField.get(this.sub2b);

        assertSame(this.sub2b.getTopSet().iterator().next(), this.sub2b);
        this.sub2b.sidekarAttach();
        assert this.sub2b.isSidekarAttached();
        assert this.attachedField.get(this.sub2b);
        this.sub2b.setBoolean(true);

        this.sub2b.getTopSet().clear();
        assertTrue(this.sub2b.getBoolean());
        this.sub2b.sidekarDetach();
        assert !this.sub2b.isSidekarAttached();
        assert !this.attachedField.get(this.sub2b);

        this.sub1d.setSub2(this.sub2b);
        assert !this.sub2b.isSidekarAttached();
        assert !this.attachedField.get(this.sub2b);
        assertSame(this.sub1d.getSub2(), this.sub2b);

        this.sub2b.sidekarAttach();
        assert this.sub2b.isSidekarAttached();
        assert this.attachedField.get(this.sub2b);
        assertFalse(this.sub2b.getBoolean());

        this.sub2b.sidekarDetach();
        assert !this.sub2b.isSidekarAttached();
        assert !this.attachedField.get(this.sub2b);

        this.sub1d.setSub2(null);
        assert !this.sub2b.isSidekarAttached();
        assert !this.attachedField.get(this.sub2b);
    }

    protected org.dellroad.sidekar.Entity<Top> getTopEntity() {
        return this.db.getEntity(Top.class);
    }

    protected org.dellroad.sidekar.Entity<Sub1> getSub1Entity() {
        return this.db.getEntity(Sub1.class);
    }

    protected org.dellroad.sidekar.Entity<Sub2> getSub2Entity() {
        return this.db.getEntity(Sub2.class);
    }

    @Override
    protected List<Class<?>> getSchemaClasses() {
        return Arrays.<Class<?>>asList(Top.class, Sub1.class, Sub2.class);
    }

    // Model classes

    @SidekarEntity
    public abstract static class Top implements Item {

        @SidekarField
        public abstract boolean getBoolean();
        public abstract void setBoolean(boolean value);

        @SidekarField
        public abstract byte getByte();
        public abstract void setByte(byte value);

        @SidekarField
        public abstract short getShort();
        public abstract void setShort(short value);

        @SidekarField
        public abstract char getChar();
        public abstract void setChar(char value);

        @SidekarField
        public abstract int getInt();
        public abstract void setInt(int value);

        @SidekarField
        public abstract float getFloat();
        public abstract void setFloat(float value);

        @SidekarField
        public abstract long getLong();
        public abstract void setLong(long value);

        @SidekarField
        public abstract double getDouble();
        public abstract void setDouble(double value);

        @SidekarField
        public abstract String getString();
        public abstract void setString(String value);

        @SidekarField
        public abstract Date getDate();
        public abstract void setDate(Date value);

        @SidekarField
        public abstract BigInteger getBigInt();
        public abstract void setBigInt(BigInteger value);

        @SidekarField
        public abstract String[] getStringArray();
        public abstract void setStringArray(String[] value);

        @SidekarField
        public abstract boolean[] getBooleanArray();
        public abstract void setBooleanArray(boolean[] value);

        @SidekarField
        public abstract byte[][] getByteMultiArray();
        public abstract void setByteMultiArray(byte[][] value);

        @SidekarField
        public abstract int[][][] getIntMultiArray();
        public abstract void setIntMultiArray(int[][][] value);

        @SidekarField
        public abstract Sub1[] getSub1Array();
        public abstract void setSub1Array(Sub1[] value);
    }

    @SidekarEntity
    public abstract static class Sub1 extends Top {

        @SidekarField
        protected abstract Sub2 getSub2();
        protected abstract void setSub2(Sub2 value);
    }

    @SidekarEntity
    public abstract static class Sub2 extends Top {

        @SidekarSet
        protected abstract Set<Sub1> getSub1Set();

        @SidekarSet
        protected abstract Set<Top> getTopSet();
    }
}

